from twisted.python import log
from twisted.internet import task, threads, reactor
from twisted.internet.defer import DeferredLock
import json

import database as db
import traceback
import inspect


loc_locks = {}
turn_timeout = 20
server = None
updaters = {}
logic_functions = {}


def logic(func):
    logic_functions[func.func_name] = func
    return func


def create_response(time, req):
    a = {"what": "response"}
    a['type'] = req.type
    a['time'] = time
    a['source'] = req.source.strid
    a['loc_id'] = req.loc_id
    if req.target:
        a['target'] = req.target.strid
    if req.target_cell:
        a['target_cell'] = str(req.target_cell.coords)
    return a


def send_environment(player, loc_id, handler):
    cells = handler.session.query(db.Cell).filter_by(loc_id=loc_id).all()
    env = {"what": "environment"}
    for c in cells:
        cdict = {"terrain": {'#':'wall','.': 'floor'}[c.char]}
        if c.objects:
            cdict.update({"objects": [o.strid for o in c.objects]})
        env["{},{}".format(*c.coords)] = cdict
    reactor.callFromThread(lambda:  #@UndefinedVariable
                           player.sendLine(json.dumps([env])))


def locking(loc_id, name = "noname", toThread = True):
    def decor(func):
        l = loc_locks[loc_id]
        d = l.acquire()
        d.name = name
        def wrapper(lock):
            fname = func.func_name+" from "+str(inspect.getfile(func))
            print "EXECUTION OF", fname, "STARTED, LOCKED:", lock.loc_id,\
            ", IN LINE:", len(l.waiting)
            try:
                func(lock)
            except:
                print "EXECUTION OF", fname, "FAILED:"
                log.err()
            finally:
                print "EXECUTION OF", fname, "ENDED, UNLOCKING:", lock.loc_id
                lock.release()
        if toThread:
            call = lambda l: threads.deferToThread(wrapper,l)
        else:
            call = wrapper
        d.addCallback(call)
        d.addErrback(lambda i:None)
        return wrapper
    return decor

def init(s):
    global server
    server = s
    session = db.Session()
    for loc in session.query(db.Location):
        # create locks and schedule updates
        l = DeferredLock()
        l.loc_id = loc.id
        loc_locks[loc.id] = l
        call=task.LoopingCall(update_in_thread, loc_id=loc.id)
        call.start(turn_timeout, False)
        updaters[loc.id] = call
    session.close()

def check_update(player):
    print "checking:", player.factory.players.values()
    if all([p.committed for p in player.factory.players.values() if p.loc_id == player.loc_id ]):
        print "last player committed, updating"
        call=updaters[player.loc_id]
        call.stop()
        call.start(turn_timeout, True)

def filter_appear(reqs, handler):
    '''Register appear request.

    If appear is one of the requirements
    creature can not do anything else
    yeah, like in MTG.
    '''
    appear = [r for r in reqs if r.type == 'appear']
    if appear:
        a=appear.pop()
        reqs.remove(a)
        if reqs:
            log.msg("WARNING: Appear ate all these requests:"+str(reqs))
            for r in reqs:
                handler.session.delete(r)
        reqs[:]=[a]

#################### LOGIC FUNCTIONS: #########################

@logic
def enter(time, request, h):
    r = create_response(time, request)
    c = request.target_cell
    loc_id = request.loc_id
    source = request.source
    cells = h.get_location(loc_id).cells

    if not c:
        c = h.find_free_cell(loc_id, source)

    targ_cells = source.would_coords(c.coords)

    if not all([cc in cells and cells[cc].passable for cc in targ_cells]):
        c = h.find_free_cell(loc_id, source)
    if not c:
        r['info'] = 'fail:could not find free cell to appear'
        return r
    r['info'] = 'ok'
    source.cell = c
    h.session.delete(request)
    return r


@logic
def move(time, request, h):
    r = create_response(time, request)
    try:
        assert request.source.cell, "no cell for creature"+str(request.source) 
        direction = db.Coord((0,0))
        for d in set(request.info.lower()):
            direction += {  
                          'n':(0,-1),
                          'w':(-1,0),
                          's':(0,1),
                          'e':(1,0)
                          }[d]
        new_coords = request.source.cell.coords + direction
        target_cell = h.get_location(request.loc_id).cells.get(new_coords, None)
        assert target_cell, "NO SUCH CELL: "+str(new_coords)
        
        print "TRYING TO MOVE", request.source, "to", repr(target_cell)

        src = request.source
        if src.fits(target_cell):
            src.cell = target_cell
            r['info']='ok'
            r['target_cell'] = str(target_cell.coords)
        else:
            r['info']='fail:creature won\'t fit in there:'
    except Exception, e:
        r['info'] = 'fail:'+repr(e)
    finally:
        h.session.delete(request)
        return r

###############################################################################

def distribute(response, players):
    # TODO: filter according to view field to all players
    msg = json.dumps([response])
    for p in players:
        reactor.callFromThread(lambda:p.sendLine(msg)) #@UndefinedVariable

def update_in_thread(loc_id):
    for d in loc_locks[loc_id].waiting:
        if not hasattr(d,'name') or d.name != 'system':
            try:
                d.cancel()
                print d.name, "cancelled"
            except:
                print "cancel failed"
    
    d=threads.deferToThread(update, loc_id)
    d.addErrback(log.err)

def not_enuf_points(time, request, players, handler):
    r = create_response(time, request)
    r['info'] = 'fail:not enough action points'
    handler.session.delete(request)
    distribute(r, players)


def update(loc_id):
    # ruthlessly remove all pending operations on this lock:

    @locking(loc_id, "update", False)
    def upd(lock):
        with db.Handler(True) as h:
            reqs = h.get_requests(loc_id)
            players = server.get_players(loc_id)
            print "updating players", players
            timed_reqs = []
            for source_id in set([r.source_id for r in reqs]):
                time = 0 # TODO: apply agility penalty and maybe some randomness
                current_reqs = [r for r in reqs if r.source_id == source_id]
                filter_appear(current_reqs, h)
                for request in current_reqs:
                    if time + request.cost > 100:
                        not_enuf_points(time, request, players, h)
                        continue
                    timed_reqs.append((time,request))
                    time += request.cost

            for i in sorted(timed_reqs):
                try:
                    response = logic_functions[request.type](i[0], i[1], h)
                    print "Action", request.type, "returned", response
                    distribute(response, players)
                except:
                    print "Action", request.type, "failed!"
                    traceback.print_exc()

            for p in players:
                p.creature.proposed_coords = None
                send_environment(p, loc_id, h)

        for d in lock.waiting:
            # all non-system calls are canceled
            if not hasattr(d,'name') or d.name != 'system':
                d.cancel()

        for p in players:
            p.committed = False

